﻿namespace HuwmanCode.Diagnostics.Logging

open System
open System.Globalization
open System.Transactions
open System.Runtime.Serialization
open System.Reflection
open System.ComponentModel.Composition
open HuwmanCode
open HuwmanCode.Transactions
open HuwmanCode.Diagnostics

type public Logger(initLogWriters:ILogWriter seq Func, auxWriter:ILogWriter, traceSwitch:System.Diagnostics.TraceSwitch) = 
    let safeWriteAux message =
        try
            message |> auxWriter.Write
        with _ -> ()

    let asyncWriters, syncWriters = 
        let compensate e =
            let error = "Error attempting to initialize log writers!"
            TraceHelper.GetTraceMessage(TraceLevel.Error, error, Some e) |> safeWriteAux
        initLogWriters.Invoke |> Fun.TryWithRethrow compensate |> Seq.toArray |> Array.partition (fun w -> match w with | :? ILogAsyncWriter -> true | _ -> false)
    let asyncWriters = asyncWriters |> Array.map(fun aw -> aw :?> ILogAsyncWriter) |> Array.toList

    let writeTrace overrideSwitchLevel (message:TraceMessage)  =
        let canWrite =
            match traceSwitch.Level with
            | System.Diagnostics.TraceLevel.Verbose -> true
            | System.Diagnostics.TraceLevel.Info when message.Level >= TraceLevel.Info -> true
            | System.Diagnostics.TraceLevel.Warning when message.Level >= TraceLevel.Warn -> true
            | System.Diagnostics.TraceLevel.Error when message.Level >= TraceLevel.Error -> true
            | _ -> false

        if canWrite || overrideSwitchLevel then
            let syncWrite message (writer:ILogWriter) =
                let compensate e =
                    let error = sprintf "Exception writing trace to logger '%s'." (writer.GetType().FullName)
                    TraceHelper.GetTraceMessage(TraceLevel.Error, error, Some e) |> safeWriteAux
                (fun _ -> writer.Write message) |> Fun.Try compensate |> Option.isNone

            let rec asyncWrite message (writers:ILogAsyncWriter list) writeErrors =
                match writers with
                | [] -> if writeErrors > 0 then safeWriteAux message
                | writer::tl -> async { 
                    try
                        do! Async.FromBeginEnd(message, writer.BeginWrite, writer.EndWrite)
                        asyncWrite message tl writeErrors
                    with e ->
                        let error = sprintf "Exception writing trace to asynchronous logger '%s'." (writer.GetType().FullName)
                        TraceHelper.GetTraceMessage(TraceLevel.Error, error, Some e) |> safeWriteAux
                        asyncWrite message tl (writeErrors + 1) } |> Async.Start

            //*** Don't let any ambient transaction affect the outcome
            use noTransactionScope = new TransactionScope(TransactionScopeOption.RequiresNew) 
            let noAsyncWriters, noSyncWriters = asyncWriters.Length = 0, syncWriters.Length = 0
            let syncWriteErrors = syncWriters |> Array.map(fun writer -> syncWrite message writer) |> Array.filter(fun e -> e = true) |> Array.length > 0
            
            match noAsyncWriters,noSyncWriters,syncWriteErrors with
            | true,true,_ -> safeWriteAux message
            | true,_,true -> safeWriteAux message
            | false,_,false -> asyncWrite message asyncWriters 0
            | false,_,true  -> asyncWrite message asyncWriters 1
            | _ -> ()
            noTransactionScope.Complete ()

    let getTrace level =
        let getMsg format args = String.Format(CultureInfo.InvariantCulture, format, args)
        let getMsgFP provider format args = String.Format(provider, format, args)

        { new ITrace with
            member this.Write(message) = (level, message, None) |> TraceHelper.GetTraceMessage |> writeTrace false
            member this.Write(message, exn) = (level, message, Some exn) |> TraceHelper.GetTraceMessage |> writeTrace false
            member this.WriteFormat(format, args) = (level, getMsg format args, None) |> TraceHelper.GetTraceMessage |> writeTrace false
            member this.WriteFormat(formatProvider, format, args) = (level, getMsgFP formatProvider format args, None) |> TraceHelper.GetTraceMessage |> writeTrace false
            
            member this.WriteFormatEx(exn, format, args) = (level, getMsg format args, Some exn) |> TraceHelper.GetTraceMessage |> writeTrace false
            member this.WriteFormatEx(exn, formatProvider, format, args) = (level, getMsgFP formatProvider format args, Some exn) |> TraceHelper.GetTraceMessage |> writeTrace false
                
            member this.WriteOnCommit(message) = CommitAction.Enlist(fun _ -> this.Write(message))
            member this.WriteFormatOnCommit(format, args) = CommitAction.Enlist(fun _ -> this.WriteFormat(format, args))
            member this.WriteFormatOnCommit(formatProvider, format, args) = CommitAction.Enlist(fun _ -> this.WriteFormat(formatProvider, format, args))
                
            member this.WriteOnRollback(message) = RollbackAction.Enlist(fun _ -> this.Write(message))
            member this.WriteFormatOnRollback(format, args) = RollbackAction.Enlist(fun _ -> this.WriteFormat(format, args))
            member this.WriteFormatOnRollback(formatProvider, format, args) = RollbackAction.Enlist(fun _ -> this.WriteFormat(formatProvider, format, args))
                
            member this.WriteOnActionError(action, failureMessage) = action |> Action.Try(Action<_>(fun e -> this.Write(failureMessage, e)))
            member this.WriteFormatOnActionError(action, format,  args) = action |> Action.Try(Action<_>(fun e -> this.WriteFormatEx(e, format, args)))
            member this.WriteFormatOnActionError(action, formatProvider, format, args) = action |> Action.Try(Action<_>(fun e -> this.WriteFormatEx(e, formatProvider, format, args)))
               
            member this.WriteOnActionErrorReraise(action, failureMessage) = action |> Action.TryWithRethrow(Action<_>(fun e -> this.Write(failureMessage, e)))
            member this.WriteFormatOnActionErrorReraise(action, format, args) = action |> Action.TryWithRethrow(Action<_>(fun e -> this.WriteFormatEx(e, format, args)))
            member this.WriteFormatOnActionErrorReraise(action, formatProvider, format:String, args) = action |> Action.TryWithRethrow(Action<_>(fun e -> this.WriteFormatEx(e, formatProvider, format, args)))
    
            member this.WriteOnFunctionError(func, failureMessage) = func |> Func.Try(Action<_>(fun e -> this.Write(failureMessage, e)))
            member this.WriteFormatOnFunctionError(func, format,  args) = func |> Func.Try(Action<_>(fun e -> this.WriteFormatEx(e, format, args)))
            member this.WriteFormatOnFunctionError(func, formatProvider, format, args) = func |> Func.Try(Action<_>(fun e -> this.WriteFormatEx(e, formatProvider, format, args)))
               
            member this.WriteOnFunctionErrorReraise(func, failureMessage) = func |> Func.TryWithRethrow(Action<_>(fun e -> this.Write(failureMessage, e)))
            member this.WriteFormatOnFunctionErrorReraise(func, format, args) = func |> Func.TryWithRethrow(Action<_>(fun e -> this.WriteFormatEx(e, format, args)))
            member this.WriteFormatOnFunctionErrorReraise(func, formatProvider, format:String, args) = func |> Func.TryWithRethrow(Action<_>(fun e -> this.WriteFormatEx(e, formatProvider, format, args))) }

    do 
        let message = sprintf "HuwmanCode.Diagnostics.Logging.Logger created, switch level is %O" traceSwitch.Level
        (TraceLevel.Info, message, None) |> TraceHelper.GetTraceMessage |> writeTrace true

    new (initLogWriters, traceSwitch) = new Logger(initLogWriters, new NullEventWriter(), traceSwitch)
    
    interface ILogger with
        member this.Debug = getTrace TraceLevel.Debug
        member this.Info = getTrace TraceLevel.Info 
        member this.Warn = getTrace TraceLevel.Warn 
        member this.Error = getTrace TraceLevel.Error 
        member this.Fatal = getTrace TraceLevel.Fatal 
        member this.Write(message) = message |> writeTrace true

    interface IDisposable with
        member this.Dispose() =
            let dispose (disposables:#IDisposable seq) = 
                disposables |> Seq.iter(fun d -> d.Dispose())
            dispose syncWriters
            dispose asyncWriters

and NullEventWriter() =
    interface ILogWriter with member x.Write m = ()
    interface IDisposable with member x.Dispose() = ()

[<Export(typeof<ILogger>)>]
[<ExportMetadata("TypeName", "HuwmanCode.Diagnostics.Logging.DefaultLogger")>]
type internal DefaultLogger () =
#if Release
    inherit Logger ((fun _ -> seq [new TraceEventWriter()]), new System.Diagnostics.TraceSwitch("HuwmanCode", null, "Info"))
#else
    inherit Logger ((fun _ -> seq [new TraceEventWriter()]), new System.Diagnostics.TraceSwitch("HuwmanCode", null, "Verbose"))
#endif

    static let instance = lazy (new DefaultLogger ())
    static member Instance = instance